﻿using System;
using System.Collections.Generic;
using System.Linq;
using NUnit.Framework;
using Moq;
using Rubberduck.Parsing.Rewriter;
using Rubberduck.VBEditor.SafeComWrappers;
using RubberduckTests.Mocks;
using Rubberduck.Refactorings.EncapsulateFieldUseBackingField;
using Rubberduck.Parsing.VBA;
using Rubberduck.Refactorings.EncapsulateField;
using Rubberduck.Parsing.Symbols;
using Rubberduck.Refactorings;
using Rubberduck.SmartIndenter;
using Rubberduck.VBEditor.SafeComWrappers.Abstract;

namespace RubberduckTests.Refactoring.EncapsulateField
{
    /// <summary>
    /// EncapsulateFieldReferenceReplacerValueFieldTests exclusively tests the reference update aspect
    /// of the EncapsulateFieldRefactoring.  So, refactored code generated by the tests do not include
    /// the encapsulation properties or modification of the target Declaration.  Only the target's IdentifierReferences 
    /// are updated (and checked).
    /// </summary>
    [TestFixture]
    public class EncapsulateFieldReferenceReplacerValueFieldTests
    {
        private const bool IsReadOnlyEncapsulation = true;
        private const bool IsReadWriteEncapsulation = false;

        [TestCase(true, "TestModule1.MyProperty = ")]
        [TestCase(false, "TestModule1.MyProperty = ")]
        [Category("Refactorings")]
        [Category("Encapsulate Field")]
        [Category(nameof(EncapsulateFieldReferenceReplacer))]
        public void PublicField_ExternalReference(bool wrapInPrivateUDT, string expectedReference)
        {
            var testTargetTuple = ("targetField", "MyProperty", IsReadWriteEncapsulation);

            var testModuleCode =
$@"
Option Explicit
Public targetField As Long";

            var declaringModule = ModuleTuple(MockVbeBuilder.TestModuleName, testModuleCode);

            var procedureModuleReferencingCode =
$@"
Option Explicit

Public Sub Bar()
    TestModule1.targetField = 7
End Sub
";
            var referencingModuleStdModule = ModuleTuple(moduleName: "StdModule", procedureModuleReferencingCode);

            var refactoredCode = TestReferenceReplacement(wrapInPrivateUDT, testTargetTuple, declaringModule, referencingModuleStdModule);

            var referencingModuleCode = refactoredCode[referencingModuleStdModule.TestModuleName];

            StringAssert.Contains(expectedReference, referencingModuleCode);
        }

        [TestCase(true, " TestModule1.MyProperty = 7")]
        [TestCase(false, " TestModule1.MyProperty = 7")]
        [Category("Refactorings")]
        [Category("Encapsulate Field")]
        [Category(nameof(EncapsulateFieldReferenceReplacer))]
        public void PublicField_ExternalWithMemberAccessReference(bool wrapInPrivateUDT, string expectedReference)
        {
            var testTargetTuple = ("targetField", "MyProperty", IsReadWriteEncapsulation);

            var testModuleCode =
$@"
Option Explicit
Public targetField As Long";

            var declaringModule = ModuleTuple("TestModule1", testModuleCode);

            var procedureModuleReferencingCode =
$@"
Option Explicit

Public Sub Bar()
    With {declaringModule.TestModuleName}
        {declaringModule.TestModuleName}.targetField = 7
    End With
End Sub
";
            var referencingModuleStdModule = ModuleTuple(moduleName: "StdModule", procedureModuleReferencingCode);

            var refactoredCode = TestReferenceReplacement(wrapInPrivateUDT, testTargetTuple, declaringModule, referencingModuleStdModule);

            var referencingModuleCode = refactoredCode[referencingModuleStdModule.TestModuleName];

            StringAssert.Contains(expectedReference, referencingModuleCode);
        }

        [TestCase(true, "Public", "MyProperty = 7", " Bars MyProperty")]
        [TestCase(false, "Public", "MyProperty = 7", " Bars MyProperty")]
        [TestCase(true, "Private", "MyProperty = 7", " Bars MyProperty")]
        [TestCase(false, "Private", "MyProperty = 7", " Bars MyProperty")]
        [Category("Refactorings")]
        [Category("Encapsulate Field")]
        [Category(nameof(EncapsulateFieldReferenceReplacer))]
        public void PublicPrivateField_LocalReferences_ReadWrite(bool wrapInPrivateUDT, string visibility, params string[]expectedContent)
        {
            var testTargetTuple = ("targetField", "MyProperty", IsReadWriteEncapsulation);

            var testModuleCode =
$@"
Option Explicit
{visibility} targetField As Long

Public Sub Bar()
    targetField = 7
    Bars targetField
End Sub

Public Sub Bars(ByVal arg As Long)
End Sub
";
            var declaringModule = ModuleTuple(MockVbeBuilder.TestModuleName, testModuleCode);

            var refactoredCode = TestReferenceReplacement(wrapInPrivateUDT, testTargetTuple, declaringModule);

            var actualCode = refactoredCode[declaringModule.TestModuleName];
            foreach (var fragment in expectedContent)
            {
                StringAssert.Contains(fragment, actualCode);
            }
        }

        [TestCase(true, "Public", "this.MyProperty = 7", " Bars MyProperty")]
        [TestCase(false, "Public", "targetField = 7", " Bars MyProperty")]
        [TestCase(true, "Private", "this.MyProperty = 7", " Bars MyProperty")]
        [TestCase(false, "Private", "targetField = 7", " Bars MyProperty")]
        [Category("Refactorings")]
        [Category("Encapsulate Field")]
        [Category(nameof(EncapsulateFieldReferenceReplacer))]
        public void PublicPrivateField_LocalReferences_ReadOnly(bool wrapInPrivateUDT, string visibility, params string[] expectedContent)
        {
            var testTargetTuple = ("targetField", "MyProperty", IsReadOnlyEncapsulation);

            var testModuleCode =
$@"
Option Explicit
{visibility} targetField As Long

Public Sub Bar()
    targetField = 7
    Bars targetField
End Sub

Public Sub Bars(ByVal arg As Long)
End Sub
";
            var declaringModule = ModuleTuple(MockVbeBuilder.TestModuleName, testModuleCode);

            var refactoredCode = TestReferenceReplacement(wrapInPrivateUDT, testTargetTuple, declaringModule);

            var actualCode = refactoredCode[declaringModule.TestModuleName];
            foreach (var fragment in expectedContent)
            {
                StringAssert.Contains(fragment, actualCode);
            }
        }

        [TestCase(true)]
        [TestCase(false)]
        [Category("Refactorings")]
        [Category("Encapsulate Field")]
        [Category(nameof(EncapsulateFieldReferenceReplacer))]
        public void PublicField_ExternalReferencesExist_IgnoresReadOnlyFlag(bool wrapInPrivateUDT)
        {
            //Simulates the scenario where the readOnly flag was set to 'True' by some means (other than the UI).
            //Since there are external references, a property Let/Set will be generated 
            //by the refactoring and the references are modified accordingly.
            //Consequently, the solution for both test cases are identical
            var testTargetTuple = ("targetField", "MyProperty", IsReadOnlyEncapsulation);

            var testModuleCode =
$@"
Option Explicit

Public targetField As Long

Private mValue As Long

Public Sub Fizz(arg As Long)
    mValue = targetField + arg
End Sub

Public Sub Bizz(arg As Long)
    targetField = arg
End Sub
";
            var declaringModule = ModuleTuple(MockVbeBuilder.TestModuleName, testModuleCode);

            var referencingModuleCode =
$@"
Option Explicit

Private mValue As Long

Public Sub Fazz(arg As Long)
    mValue = targetField * arg
End Sub

Public Sub Fazzle(arg As Long)
    targetField = arg * mValue
End Sub
";

            var referencingModuleStdModule = ModuleTuple(moduleName: "SomeOtherModule", referencingModuleCode);

            var refactoredCode = TestReferenceReplacement(wrapInPrivateUDT, testTargetTuple, declaringModule, referencingModuleStdModule);

            var expectedDeclaringModuleRead = "mValue = MyProperty + arg";
            var expectedDeclaringModuleWrite = "MyProperty = arg";

            var declaringModuleResults = refactoredCode[declaringModule.TestModuleName];
            StringAssert.Contains(expectedDeclaringModuleRead, declaringModuleResults);
            StringAssert.Contains(expectedDeclaringModuleWrite, declaringModuleResults);

            var expectedReferencingModuleRead = $"mValue = {declaringModule.TestModuleName}.MyProperty * arg";
            var expectedReferencingModuleWrite = $" {declaringModule.TestModuleName}.MyProperty = arg * mValue";

            var referencingModuleResults = refactoredCode[referencingModuleStdModule.TestModuleName];
            StringAssert.Contains(expectedReferencingModuleRead, referencingModuleResults);
            StringAssert.Contains(expectedReferencingModuleWrite, referencingModuleResults);
        }

        [TestCase(true, ".Fizz = 0", "Bar .Fizz", " theClass.Fizz = 0", "Bar theClass.Fizz")]
        [TestCase(false, ".Fizz = 0", "Bar .Fizz", " theClass.Fizz = 0", "Bar theClass.Fizz")]
        [Category("Refactorings")]
        [Category("Encapsulate Field")]
         public void PublicFieldOfClassModule_ExternalReferences(bool wrapInPrivateUDT, params string[] expectedContent)
        {
            var testTargetTuple = ("mFizz", "Fizz", IsReadWriteEncapsulation);

            var testModuleCode =
@"Public mFizz As Integer";

            var declaringModule = ModuleTuple(MockVbeBuilder.TestModuleName, testModuleCode, ComponentType.ClassModule);

            var referencingModuleCode =
$@"Sub Bazz()
    With new {declaringModule.TestModuleName}
        .mFizz = 0
        Bar .mFizz
    End With
End Sub

Sub Bizz()
    Dim theClass As {declaringModule.TestModuleName}
    Set theClass = new {declaringModule.TestModuleName}
    theClass.mFizz = 0
    Bar theClass.mFizz
End Sub

Sub Bar(ByVal v As Integer)
End Sub";
            
            var referencingModuleStdModule = ModuleTuple(moduleName: "SomeOtherModule", referencingModuleCode);

            var refactoredCode = TestReferenceReplacement(wrapInPrivateUDT, testTargetTuple, declaringModule, referencingModuleStdModule);

            var actualCode = refactoredCode[referencingModuleStdModule.TestModuleName];
            foreach (var fragment in expectedContent)
            {
                StringAssert.Contains(fragment, actualCode);
            }
            StringAssert.DoesNotContain("mFizz", actualCode);
        }

        private static (string TestModuleName, string TargetID, ComponentType ComponentType) ModuleTuple(string moduleName, string targetName, ComponentType componentType = ComponentType.StandardModule)
            => (moduleName, targetName, componentType);

        private static IDictionary<string, string> TestReferenceReplacement(bool wrapInPrivateUDT, (string, string, bool) testTargetTuple, params (string, string, ComponentType)[] moduleTuples)
        {
            return ReferenceReplacerTestSupport.TestReferenceReplacement(wrapInPrivateUDT, testTargetTuple, moduleTuples);
        }
    }
}
